# Author: Chris Moyer
from marajo.exceptions import NotFound, Unauthorized
from marajo.appengine.handlers import RequestHandler
from marajo.appengine.api import users

import boto
from boto.utils import find_class, Password
from marajo.appengine.ext.db import Model, Key, Property

import re
from datetime import datetime

import logging
log = logging.getLogger("marajo.db")

class DBHandler(RequestHandler):
	"""DB Handler Base class
	this provides a simple CRUD interface to 
	any DB object

	The simplest use of this handler would be calling it directly as such::
		- url: /blog(\.[^\?]*)?(.*)
		  handler: marajo.appengine.handlers.db.DBHandler
		  db_class: resources.post.Post

	You may also pass in the follwoing custom fields:
	* db_class: Required, the class to use for this interface
	* list_template: Optional, custom template to use for listing
	* search_template: Optional, custom template to display to search
	* create_template: Optional, custom template to use when creating an object
	* edit_template: Optional, custom template to use when editing an object
	"""
	db_class = None
	template = None
	list_template = None
	search_template = None
	edit_template = None
	create_template = None
	MAX_LIMIT = 50 # Max results to return on an average query

	def __init__(self, config, request, response):
		RequestHandler.__init__(self, config, request, response)
		db_class_name = self.config.get('db_class', None)
		if db_class_name:
			self.db_class = find_class(db_class_name)
		self.list_template = self.config.get("list_template", "db/list_template.tmpl")
		self.search_template = self.config.get("search_template", "db/form_template.tmpl")
		self.edit_template = self.config.get("edit_template", "db/form_template.tmpl")
		self.create_template = self.config.get("create_template", "db/form_template.tmpl")

	def get(self):
		"""Get an object, or search for a list of objects"""
		action = self.request.path_info.strip('/')
		if action == 'search':
			return self.search_form()
		elif action == 'create':
			return self.create_form()
		elif action == "index":
			return self.index()
		else:
			obj = self.read()
			if obj:
				# Read
				return self.edit_form(obj)
			else:
				# Display the appropriate objects in list format
				return self.list()

	def index(self):
		"""
		By default our index page just lists everything
		"""
		return self.list()

	def list(self):
		properties = self.db_class.properties()
		d = {"objects": self.search(self.request.GET), "properties": properties, "action_href": self.request.script_name}
		self.display(self.list_template, d)

	def post(self):
		"""Save or update object to the DB"""
		if not self.user:
			raise Unauthorized()
		obj = self.read()
		if obj:
			log.info("===== %s Update %s =====" % (self.user.username, self.db_class.__class__.__name__))
			obj = self.update(obj, self.request.POST)
			log.info("========================")
		else:
			log.info("===== %s Create %s =====" % (self.user.username, self.db_class.__class__.__name__))
			obj = self.create(self.request.POST)
			log.info("========================")
		return self.redirect("%s/%s" % (self.request.script_name, obj.id))

	def display(self, template_name, values={}):
		if self.request.file_extension == "json":
			try:
				import json
			except:
				import simplejson as json
			if values.has_key("objects"):
				ret = {}
				ret['objects'] = [o for o in values['objects']]
				if values['objects'].next_token:
					ret["next_token"] = values['objects'].next_token
				values = ret
			elif values.has_key("object"):
				values = values['object']
			self.response.content_type = "application/json"
			self.response.write(json.dumps(self.encode_json(values)))
		else:
			RequestHandler.display(self, template_name, values)

	def decode(self, type, value):
		"""Decode a string value sent by the user into actual things"""
		if value:
			if Model in type.mro():
				return type.get_by_ids(value)
			elif type == bool:
				if value.lower() == 'true':
					return True
				else:
					return False
			elif type == datetime:
				return datetime.strptime(value, "%m/%d/%y %H:%M:%S")
			elif type == Password:
				return value
			else:
				return type(value)
		return None

	def encode_json(self, val):
		"""Encode this list or object to json"""
		if isinstance(val, dict):
			ret = {}
			for k in val.keys():
				ret[k] = self.encode_json(val[k])
			return ret
		elif isinstance(val, list):
			return [self.encode_json(v) for v in val]
		elif isinstance(val, self.db_class):
			ret = {}
			for prop in val.properties():
				if prop.name:
					ret[prop.name] = self.encode_json(getattr(val, prop.name))
			ret['id'] = val.id
			return ret
		else:
			return str(val)


	def search_form(self):
		# Get a list of the properties to display here
		properties = self.db_class.properties()
		d = {'properties' : properties,
			 'action_method' : 'GET',
			 'action_label' : 'Search',
			 'obj' : self.db_class(),
			 'action_href' : self.request.script_name}
		self.request.path
		self.display(self.search_template, d)

	def edit_form(self, obj):
		# Get a list of the properties to display here
		properties = self.db_class.properties()
		d = {'properties' : properties,
			 'action_method' : 'POST',
			 'action_label' : 'Submit',
			 'action_href' : "%s/%s" % (self.request.script_name, obj.id),
			 'obj' : obj}
		self.display(self.edit_template, d)

	def create_form(self):
		# Get a list of the properties to display here
		properties = self.db_class.properties()
		d = {'properties' : properties,
			 'action_method' : 'POST',
			 'action_label' : 'Create',
			 'action_href' : self.request.script_name,
			 'obj': self.db_class() }
		self.display(self.create_template, d)

	def search(self, search_terms):
		"""Search for given objects
		:param search_terms: The Terms to search for
		:type search_terms: MultiDict
		"""
		query = self.db_class.find()
		for filter in set(search_terms.keys()):
			if filter == "next_token":
				query.next_token = search_terms.get(filter).replace(" ", "+")
				continue
			filter_value = search_terms.getall(filter)
			filter_args = filter.split(".")
			if len(filter_args) > 1:
				filter_cmp = filter_args[1]
			else:
				filter_cmp = "="
			filter = filter_args[0]
			if len(filter_value) == 1:
				filter_value = filter_value[0]
			if filter_value:
				query.filter("%s %s " % (filter, filter_cmp), filter_value)
		query.limit = self.MAX_LIMIT
		return query

	def create(self, params):
		"""Create an object in the DB
		:param params: Dictionary of params to set on this object
		:type params: dict
		"""
		id = None
		if params.has_key("id"):
			id = params['id']
		obj = self.db_class(id=id)
		return self.update(obj, params)

	def read(self):
		"""Get the object that this URI points to, or None if they don't point to one
		If we point to a URI that doesn't exist, we toss a NotFound error
		"""
		obj = None
		if(self.request.match_groups and len(self.request.match_groups[-1]) > 0):
			obj_id = self.request.match_groups[-1].strip("/") # Remove leading and trailing "/" if they exist
			try:
				obj = self.db_class.get_by_ids(obj_id)
			except:
				raise NotFound()
		return obj

	def update(self, obj, params):
		"""Update our object
		:param params: Dictionary of params to set on this object
		:type params: dict
		"""
		props = {}
		for p in obj.properties():
			props[p.name] = p
		obj._auto_update = False
		for param in params.keys():
			if props.has_key(param):
				prop = props[param]
				if prop.data_type == list:
					p_list = params.getall(param)
					if len(p_list) <= 1:
						p_list = p_list[0].split("\r\n")
					val = []
					for v in p_list:
						if v != None:
							val.append(self.decode(prop.item_type, v))
				elif prop.data_type == Key:
					val = self.decode(prop.reference_class, params.getone(param))
				else:
					val = self.decode(prop.data_type, params.getone(param))
				if val != None and val != [None]:
					log.info("%s: %s" % (param, str(val)))
					setattr(obj, param, val)
		obj.save()
		return obj


	def delete(self):
		"""Delete a given object"""
		obj = self.read();
		obj.delete()
		log.info("%s Deleted object %s" % (self.user.username, obj.id))
		self.response.write(obj.id)
		return obj


